/******************************************************************************* * Copyright (c) 2000, 2016 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation * Lars Vogel <Lars.Vogel@gmail.com> - Bug 440810 * Patrik Suzzi <psuzzi@gmail.com> - Bug 489250 *******************************************************************************/ package org.eclipse.ui.internal.keys; import static org.eclipse.swt.events.SelectionListener.widgetDefaultSelectedAdapter; import static org.eclipse.swt.events.SelectionListener.widgetSelectedAdapter; import com.ibm.icu.text.Collator; import com.ibm.icu.text.MessageFormat; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.ResourceBundle; import java.util.Set; import org.eclipse.core.commands.Category; import org.eclipse.core.commands.Command; import org.eclipse.core.commands.CommandManager; import org.eclipse.core.commands.ParameterizedCommand; import org.eclipse.core.commands.common.NotDefinedException; import org.eclipse.core.commands.contexts.Context; import org.eclipse.core.commands.contexts.ContextManager; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.SafeRunner; import org.eclipse.core.runtime.Status; import org.eclipse.jface.bindings.Binding; import org.eclipse.jface.bindings.BindingManager; import org.eclipse.jface.bindings.Scheme; import org.eclipse.jface.bindings.TriggerSequence; import org.eclipse.jface.bindings.keys.KeyBinding; import org.eclipse.jface.bindings.keys.KeySequence; import org.eclipse.jface.bindings.keys.KeySequenceText; import org.eclipse.jface.bindings.keys.KeyStroke; import org.eclipse.jface.contexts.IContextIds; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.preference.PreferencePage; import org.eclipse.jface.util.SafeRunnable; import org.eclipse.swt.SWT; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Combo; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.FileDialog; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swt.widgets.TabFolder; import org.eclipse.swt.widgets.TabItem; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TableItem; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.IWorkbench; import org.eclipse.ui.IWorkbenchPreferencePage; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.activities.IActivityManager; import org.eclipse.ui.commands.ICommandService; import org.eclipse.ui.contexts.IContextService; import org.eclipse.ui.internal.IPreferenceConstants; import org.eclipse.ui.internal.IWorkbenchHelpContextIds; import org.eclipse.ui.internal.WorkbenchPlugin; import org.eclipse.ui.internal.misc.StatusUtil; import org.eclipse.ui.internal.util.PrefUtil; import org.eclipse.ui.internal.util.Util; import org.eclipse.ui.keys.IBindingService; import org.eclipse.ui.statushandlers.StatusManager; /** * The preference page for defining keyboard shortcuts. While some of its * underpinning have been made generic to "bindings" rather than "key bindings", * it will still take some work to remove the link entirely. * * @since 3.0 */ public final class KeysPreferencePage extends PreferencePage implements IWorkbenchPreferencePage { /** * A selection listener to be used on the columns in the table on the view * tab. This selection listener modifies the sort order so that the * appropriate column is in the first position. * * @since 3.1 */ private class SortOrderSelectionListener extends SelectionAdapter { /** * The column to be put in the first position. This value should be one * of the constants defined by <code>SORT_COLUMN_</code>. */ private final int columnSelected; /** * Constructs a new instance of <code>SortOrderSelectionListener</code>. * * @param columnSelected * The column to be given first priority in the sort order; * this value should be one of the constants defined as * <code>SORT_COLUMN_</code>. */ private SortOrderSelectionListener(final int columnSelected) { this.columnSelected = columnSelected; } @Override public void widgetSelected(SelectionEvent e) { // Change the column titles. final int oldSortIndex = sortOrder[0]; final TableColumn oldSortColumn = tableBindings .getColumn(oldSortIndex); oldSortColumn.setText(UNSORTED_COLUMN_NAMES[oldSortIndex]); final TableColumn newSortColumn = tableBindings .getColumn(columnSelected); newSortColumn.setText(SORTED_COLUMN_NAMES[columnSelected]); // Change the sort order. boolean columnPlaced = false; boolean enoughRoom = false; int bumpedColumn = -1; for (int i = 0; i < sortOrder.length; i++) { if (sortOrder[i] == columnSelected) { /* * We've found the place where the column existing in the * old sort order. No matter what at this point, we have * completed the reshuffling. */ enoughRoom = true; if (bumpedColumn != -1) { // We have already started bumping things around, so // drop the last bumped column here. sortOrder[i] = bumpedColumn; } else { // The order has not changed. columnPlaced = true; } break; } else if (columnPlaced) { // We are currently bumping, so just bump another. int temp = sortOrder[i]; sortOrder[i] = bumpedColumn; bumpedColumn = temp; } else { /* * We are not currently bumping, so drop the column and * start bumping. */ bumpedColumn = sortOrder[i]; sortOrder[i] = columnSelected; columnPlaced = true; } } // Grow the sort order. if (!enoughRoom) { final int[] newSortOrder = new int[sortOrder.length + 1]; System.arraycopy(sortOrder, 0, newSortOrder, 0, sortOrder.length); newSortOrder[sortOrder.length] = bumpedColumn; sortOrder = newSortOrder; } // Update the view tab. updateViewTab(); } } /** * The data key for the binding stored on an SWT widget. The key is a * fully-qualified name, but in reverse order. This is so that the equals * method will detect misses faster. */ private static final String BINDING_KEY = "Binding.bindings.jface.eclipse.org"; //$NON-NLS-1$ /** * The image associate with a binding that exists as part of the system * definition. */ private static final Image IMAGE_BLANK = ImageFactory.getImage("blank"); //$NON-NLS-1$ /** * The image associated with a binding changed by the user. */ private static final Image IMAGE_CHANGE = ImageFactory.getImage("change"); //$NON-NLS-1$ /** * The data key at which the <code>Binding</code> instance for a table * item is stored. */ private static final String ITEM_DATA_KEY = "org.eclipse.jface.bindings"; //$NON-NLS-1$ /** * The number of items to show in the combo boxes. */ private static final int ITEMS_TO_SHOW = 9; /** * The resource bundle from which translations can be retrieved. */ private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle .getBundle(KeysPreferencePage.class.getName()); /** * The total number of columns on the view tab. */ private static final int VIEW_TOTAL_COLUMNS = 4; /** * The translated names for the columns when they are the primary sort key * (e.g., ">Category<"). */ private static final String[] SORTED_COLUMN_NAMES = new String[VIEW_TOTAL_COLUMNS]; /** * The index of the modify tab. * * @since 3.1 */ private static final int TAB_INDEX_MODIFY = 1; /** * The translated names for the columns when they are not the primary sort * key (e.g., "Category"). */ private static final String[] UNSORTED_COLUMN_NAMES = new String[VIEW_TOTAL_COLUMNS]; /** * The index of the column on the view tab containing the category name. */ private static final int VIEW_CATEGORY_COLUMN_INDEX = 0; /** * The index of the column on the view tab containing the command name. */ private static final int VIEW_COMMAND_COLUMN_INDEX = 1; /** * The index of the column on the view tab containing the context name. */ private static final int VIEW_CONTEXT_COLUMN_INDEX = 3; /** * The index of the column on the view tab containing the key sequence. */ private static final int VIEW_KEY_SEQUENCE_COLUMN_INDEX = 2; static { UNSORTED_COLUMN_NAMES[VIEW_CATEGORY_COLUMN_INDEX] = Util .translateString(RESOURCE_BUNDLE, "tableColumnCategory"); //$NON-NLS-1$ UNSORTED_COLUMN_NAMES[VIEW_COMMAND_COLUMN_INDEX] = Util .translateString(RESOURCE_BUNDLE, "tableColumnCommand"); //$NON-NLS-1$ UNSORTED_COLUMN_NAMES[VIEW_KEY_SEQUENCE_COLUMN_INDEX] = Util .translateString(RESOURCE_BUNDLE, "tableColumnKeySequence"); //$NON-NLS-1$ UNSORTED_COLUMN_NAMES[VIEW_CONTEXT_COLUMN_INDEX] = Util .translateString(RESOURCE_BUNDLE, "tableColumnContext"); //$NON-NLS-1$ SORTED_COLUMN_NAMES[VIEW_CATEGORY_COLUMN_INDEX] = Util.translateString( RESOURCE_BUNDLE, "tableColumnCategorySorted"); //$NON-NLS-1$ SORTED_COLUMN_NAMES[VIEW_COMMAND_COLUMN_INDEX] = Util.translateString( RESOURCE_BUNDLE, "tableColumnCommandSorted"); //$NON-NLS-1$ SORTED_COLUMN_NAMES[VIEW_KEY_SEQUENCE_COLUMN_INDEX] = Util .translateString(RESOURCE_BUNDLE, "tableColumnKeySequenceSorted"); //$NON-NLS-1$ SORTED_COLUMN_NAMES[VIEW_CONTEXT_COLUMN_INDEX] = Util.translateString( RESOURCE_BUNDLE, "tableColumnContextSorted"); //$NON-NLS-1$ } /** * The workbench's activity manager. This activity manager is used to see if * certain commands should be filtered from the user interface. */ private IActivityManager activityManager; /** * The workbench's binding service. This binding service is used to access * the current set of bindings, and to persist changes. */ private IBindingService bindingService; /** * The add button located on the bottom left of the preference page. This * button adds the current trigger sequence to the currently selected * command. */ private Button buttonAdd; /** * The remove button located on the bottom left of the preference page. This * button removes the current trigger sequence from the current command. */ private Button buttonRemove; /** * The restore button located on the bottom left of the preference page. * This button attempts to restore the currently trigger sequence to its * initial (i.e., Binding.SYSTEM) state -- undoing all user modifications. */ private Button buttonRestore; /** * A map of all the category identifiers indexed by the names that appear in * the user interface. This look-up table is built during initialization. */ private Map categoryIdsByUniqueName; /** * A map of all the category names in the user interface indexed by their * identifiers. This look-up table is built during initialization. */ private Map categoryUniqueNamesById; /** * The combo box containing the list of all categories for commands. */ private Combo comboCategory; /** * The combo box containing the list of commands relevent for the currently * selected category. */ private Combo comboCommand; /** * The combo box containing the list of contexts in the system. */ private Combo comboContext; /** * The combo box containing the list of schemes in the system. */ private Combo comboScheme; /** * A map of all the command identifiers indexed by the categories to which * they belong. This look-up table is built during initialization. */ private Map commandIdsByCategoryId; /** * The parameterized commands corresponding to the current contents of * <code>comboCommand</code>. The commands in this array are in the same * order as in the combo. This value can be <code>null</code> if nothing * is selected in the combo. */ private ParameterizedCommand[] commands = null; /** * The workbench's command service. This command service is used to access * the list of commands. */ private ICommandService commandService; /** * A map of all the context identifiers indexed by the names that appear in * the user interface. This look-up table is built during initialization. */ private Map contextIdsByUniqueName; /** * The workbench's context service. This context service is used to access * the list of contexts. */ private IContextService contextService; /** * A map of all the category names in the user interface indexed by their * identifiers. This look-up table is built during initialization. */ private Map contextUniqueNamesById; /** * The workbench's help system. This is used to register the page with the * help system. * * TODO Add a help context */ // private IWorkbenchHelpSystem helpSystem; /** * This is the label next to the table showing the bindings matching a * particular command. The label is disabled if there isn't a selected * command identifier. */ private Label labelBindingsForCommand; /** * This is the label next to the table showing the bindings matching a * particular trigger sequence. The label is disabled if there isn't a * current key sequence. */ private Label labelBindingsForTriggerSequence; /** * The label next to the context combo box. This label indicates whether the * context is a child of another context. If the current context is not a * child, then this label is blank. */ private Label labelContextExtends; /** * The label next to the scheme combo box. This label indicates whether the * scheme is a child of another scheme. If the current scheme is not a * child, then this label is blank. */ private Label labelSchemeExtends; /** * A binding manager local to this preference page. When the page is * initialized, the current bindings are read out from the binding service * and placed in this manager. This manager is then updated as the user * makes changes. When the user has finished, the contents of this manager * are compared with the contents of the binding service. The changes are * then persisted. */ private final BindingManager localChangeManager = new BindingManager( new ContextManager(), new CommandManager()); /** * A map of all the scheme identifiers indexed by the names that appear in * the user interface. This look-up table is built during initialization. */ private Map schemeIdsByUniqueName; /** * A map of all the scheme names in the user interface indexed by their * identifiers. This look-up table is built during initialization. */ private Map schemeUniqueNamesById; /** * The sort order to be used on the view tab to display all of the key * bindings. This sort order can be changed by the user. This array is never * <code>null</code>, but may be empty. */ private int[] sortOrder = { VIEW_CATEGORY_COLUMN_INDEX, VIEW_COMMAND_COLUMN_INDEX, VIEW_KEY_SEQUENCE_COLUMN_INDEX, VIEW_CONTEXT_COLUMN_INDEX }; /** * The top-most tab folder for the preference page -- containing a view and * a modify tab. */ private TabFolder tabFolder; /** * A table of the key bindings currently defined. This table appears on the * view tab; it is intended to be an easy way for users to learn the key * bindings in Eclipse. This value is only <code>null</code> until the * controls are first created. */ private Table tableBindings; /** * The table containing all of the bindings matching the selected command. */ private Table tableBindingsForCommand; /** * The table containing all of the bindings matching the current trigger * sequence. */ private Table tableBindingsForTriggerSequence; /** * The text widget where keys are entered. This widget is managed by * <code>textTriggerSequenceManager</code>, which provides its special * behaviour. */ private Text textTriggerSequence; /** * The manager for the text widget that traps incoming key events. This * manager should be used to access the widget, rather than accessing the * widget directly. */ private KeySequenceText textTriggerSequenceManager; @Override public void applyData(Object data) { if(data instanceof Binding) { editBinding((Binding) data); } } @Override protected final Control createContents(final Composite parent) { PlatformUI.getWorkbench().getHelpSystem() .setHelp(parent, IWorkbenchHelpContextIds.KEYS_PREFERENCE_PAGE); tabFolder = new TabFolder(parent, SWT.NULL); // View tab final TabItem viewTab = new TabItem(tabFolder, SWT.NULL); viewTab.setText(Util.translateString(RESOURCE_BUNDLE, "viewTab.Text")); //$NON-NLS-1$ viewTab.setControl(createViewTab(tabFolder)); // Modify tab final TabItem modifyTab = new TabItem(tabFolder, SWT.NULL); modifyTab.setText(Util.translateString(RESOURCE_BUNDLE, "modifyTab.Text")); //$NON-NLS-1$ modifyTab.setControl(createModifyTab(tabFolder)); // Do some fancy stuff. applyDialogFont(tabFolder); final IPreferenceStore store = getPreferenceStore(); final int selectedTab = store .getInt(IPreferenceConstants.KEYS_PREFERENCE_SELECTED_TAB); if ((tabFolder.getItemCount() > selectedTab) && (selectedTab > 0)) { tabFolder.setSelection(selectedTab); } return tabFolder; } /** * Creates the tab that allows the user to change the keyboard shortcuts. * * @param parent * The tab folder in which the tab should be created; must not be * <code>null</code>. * @return The composite which represents the contents of the tab; never * <code>null</code>. */ private final Composite createModifyTab(final TabFolder parent) { final Composite composite = new Composite(parent, SWT.NULL); composite.setLayout(new GridLayout()); GridData gridData = new GridData(GridData.FILL_BOTH); composite.setLayoutData(gridData); final Composite compositeKeyConfiguration = new Composite(composite, SWT.NULL); GridLayout gridLayout = new GridLayout(); gridLayout.numColumns = 3; compositeKeyConfiguration.setLayout(gridLayout); gridData = new GridData(GridData.FILL_HORIZONTAL); compositeKeyConfiguration.setLayoutData(gridData); final Label labelKeyConfiguration = new Label( compositeKeyConfiguration, SWT.LEFT); labelKeyConfiguration.setText(Util.translateString(RESOURCE_BUNDLE, "labelScheme")); //$NON-NLS-1$ comboScheme = new Combo(compositeKeyConfiguration, SWT.READ_ONLY); gridData = new GridData(); gridData.widthHint = 200; comboScheme.setLayoutData(gridData); comboScheme.setVisibleItemCount(ITEMS_TO_SHOW); comboScheme.addSelectionListener(widgetSelectedAdapter(e -> selectedComboScheme())); labelSchemeExtends = new Label(compositeKeyConfiguration, SWT.LEFT); gridData = new GridData(GridData.FILL_HORIZONTAL); labelSchemeExtends.setLayoutData(gridData); final Control spacer = new Composite(composite, SWT.NULL); gridData = new GridData(); gridData.heightHint = 10; gridData.widthHint = 10; spacer.setLayoutData(gridData); final Group groupCommand = new Group(composite, SWT.SHADOW_NONE); gridLayout = new GridLayout(); gridLayout.numColumns = 3; groupCommand.setLayout(gridLayout); gridData = new GridData(GridData.FILL_BOTH); groupCommand.setLayoutData(gridData); groupCommand.setText(Util.translateString(RESOURCE_BUNDLE, "groupCommand")); //$NON-NLS-1$ final Label labelCategory = new Label(groupCommand, SWT.LEFT); gridData = new GridData(); labelCategory.setLayoutData(gridData); labelCategory.setText(Util.translateString(RESOURCE_BUNDLE, "labelCategory")); //$NON-NLS-1$ comboCategory = new Combo(groupCommand, SWT.READ_ONLY); gridData = new GridData(); gridData.horizontalSpan = 2; gridData.widthHint = 200; comboCategory.setLayoutData(gridData); comboCategory.setVisibleItemCount(ITEMS_TO_SHOW); comboCategory.addSelectionListener(widgetSelectedAdapter(e -> update())); final Label labelCommand = new Label(groupCommand, SWT.LEFT); gridData = new GridData(); labelCommand.setLayoutData(gridData); labelCommand.setText(Util.translateString(RESOURCE_BUNDLE, "labelCommand")); //$NON-NLS-1$ comboCommand = new Combo(groupCommand, SWT.READ_ONLY); gridData = new GridData(); gridData.horizontalSpan = 2; gridData.widthHint = 300; comboCommand.setLayoutData(gridData); comboCommand.setVisibleItemCount(9); comboCommand.addSelectionListener(widgetSelectedAdapter(e -> update())); labelBindingsForCommand = new Label(groupCommand, SWT.LEFT); gridData = new GridData(GridData.VERTICAL_ALIGN_BEGINNING); gridData.verticalAlignment = GridData.FILL_VERTICAL; labelBindingsForCommand.setLayoutData(gridData); labelBindingsForCommand.setText(Util.translateString(RESOURCE_BUNDLE, "labelAssignmentsForCommand")); //$NON-NLS-1$ tableBindingsForCommand = new Table(groupCommand, SWT.BORDER | SWT.FULL_SELECTION | SWT.H_SCROLL | SWT.V_SCROLL); tableBindingsForCommand.setHeaderVisible(true); gridData = new GridData(GridData.FILL_BOTH); gridData.heightHint = 60; gridData.horizontalSpan = 2; boolean isMac = org.eclipse.jface.util.Util.isMac(); gridData.widthHint = isMac ? 620 : 520; tableBindingsForCommand.setLayoutData(gridData); TableColumn tableColumnDelta = new TableColumn(tableBindingsForCommand, SWT.NULL, 0); tableColumnDelta.setResizable(false); tableColumnDelta.setText(Util.ZERO_LENGTH_STRING); tableColumnDelta.setWidth(20); TableColumn tableColumnContext = new TableColumn( tableBindingsForCommand, SWT.NULL, 1); tableColumnContext.setResizable(true); tableColumnContext.setText(Util.translateString(RESOURCE_BUNDLE, "tableColumnContext")); //$NON-NLS-1$ tableColumnContext.pack(); tableColumnContext.setWidth(200); final TableColumn tableColumnKeySequence = new TableColumn( tableBindingsForCommand, SWT.NULL, 2); tableColumnKeySequence.setResizable(true); tableColumnKeySequence.setText(Util.translateString(RESOURCE_BUNDLE, "tableColumnKeySequence")); //$NON-NLS-1$ tableColumnKeySequence.pack(); tableColumnKeySequence.setWidth(300); tableBindingsForCommand.addMouseListener(new MouseAdapter() { @Override public void mouseDoubleClick(MouseEvent mouseEvent) { update(); } }); tableBindingsForCommand.addSelectionListener(widgetSelectedAdapter(selectionEvent -> selectedTableBindingsForCommand())); final Group groupKeySequence = new Group(composite, SWT.SHADOW_NONE); gridLayout = new GridLayout(); gridLayout.numColumns = 4; groupKeySequence.setLayout(gridLayout); gridData = new GridData(GridData.FILL_BOTH); groupKeySequence.setLayoutData(gridData); groupKeySequence.setText(Util.translateString(RESOURCE_BUNDLE, "groupKeySequence")); //$NON-NLS-1$ final Label labelKeySequence = new Label(groupKeySequence, SWT.LEFT); gridData = new GridData(); labelKeySequence.setLayoutData(gridData); labelKeySequence.setText(Util.translateString(RESOURCE_BUNDLE, "labelKeySequence")); //$NON-NLS-1$ // The text widget into which the key strokes will be entered. textTriggerSequence = new Text(groupKeySequence, SWT.BORDER); // On MacOS X, this font will be changed by KeySequenceText textTriggerSequence.setFont(groupKeySequence.getFont()); gridData = new GridData(); gridData.horizontalSpan = 2; gridData.widthHint = 300; textTriggerSequence.setLayoutData(gridData); textTriggerSequence.addModifyListener(e -> update()); textTriggerSequence.addFocusListener(new FocusListener() { @Override public void focusGained(FocusEvent e) { bindingService.setKeyFilterEnabled(false); } @Override public void focusLost(FocusEvent e) { bindingService.setKeyFilterEnabled(true); } }); textTriggerSequence.addDisposeListener(e -> { if (!bindingService.isKeyFilterEnabled()) { bindingService.setKeyFilterEnabled(true); } }); // The manager for the key sequence text widget. textTriggerSequenceManager = new KeySequenceText(textTriggerSequence); textTriggerSequenceManager.setKeyStrokeLimit(4); // Button for adding trapped key strokes final Button buttonAddKey = new Button(groupKeySequence, SWT.LEFT | SWT.ARROW); buttonAddKey.setToolTipText(Util.translateString(RESOURCE_BUNDLE, "buttonAddKey.ToolTipText")); //$NON-NLS-1$ gridData = new GridData(); gridData.heightHint = comboCategory.getTextHeight(); buttonAddKey.setLayoutData(gridData); // Arrow buttons aren't normally added to the tab list. Let's fix that. final Control[] tabStops = groupKeySequence.getTabList(); final ArrayList newTabStops = new ArrayList(); for (Control tabStop : tabStops) { newTabStops.add(tabStop); if (textTriggerSequence.equals(tabStop)) { newTabStops.add(buttonAddKey); } } final Control[] newTabStopArray = (Control[]) newTabStops .toArray(new Control[newTabStops.size()]); groupKeySequence.setTabList(newTabStopArray); // Construct the menu to attach to the above button. final Menu menuButtonAddKey = new Menu(buttonAddKey); final Iterator trappedKeyItr = KeySequenceText.TRAPPED_KEYS.iterator(); while (trappedKeyItr.hasNext()) { final KeyStroke trappedKey = (KeyStroke) trappedKeyItr.next(); final MenuItem menuItem = new MenuItem(menuButtonAddKey, SWT.PUSH); menuItem.setText(trappedKey.format()); menuItem.addSelectionListener(widgetSelectedAdapter(e -> { textTriggerSequenceManager.insert(trappedKey); textTriggerSequence.setFocus(); textTriggerSequence.setSelection(textTriggerSequence.getTextLimit()); })); } buttonAddKey.addSelectionListener(widgetSelectedAdapter(selectionEvent -> { Point buttonLocation = buttonAddKey.getLocation(); buttonLocation = groupKeySequence.toDisplay(buttonLocation.x, buttonLocation.y); Point buttonSize = buttonAddKey.getSize(); menuButtonAddKey.setLocation(buttonLocation.x, buttonLocation.y + buttonSize.y); menuButtonAddKey.setVisible(true); })); labelBindingsForTriggerSequence = new Label(groupKeySequence, SWT.LEFT); gridData = new GridData(GridData.VERTICAL_ALIGN_BEGINNING); gridData.verticalAlignment = GridData.FILL_VERTICAL; labelBindingsForTriggerSequence.setLayoutData(gridData); labelBindingsForTriggerSequence.setText(Util.translateString( RESOURCE_BUNDLE, "labelAssignmentsForKeySequence")); //$NON-NLS-1$ tableBindingsForTriggerSequence = new Table(groupKeySequence, SWT.BORDER | SWT.FULL_SELECTION | SWT.H_SCROLL | SWT.V_SCROLL); tableBindingsForTriggerSequence.setHeaderVisible(true); gridData = new GridData(GridData.FILL_BOTH); gridData.heightHint = 60; gridData.horizontalSpan = 3; gridData.widthHint = isMac ? 620 : 520; tableBindingsForTriggerSequence.setLayoutData(gridData); tableColumnDelta = new TableColumn(tableBindingsForTriggerSequence, SWT.NULL, 0); tableColumnDelta.setResizable(false); tableColumnDelta.setText(Util.ZERO_LENGTH_STRING); tableColumnDelta.setWidth(20); tableColumnContext = new TableColumn(tableBindingsForTriggerSequence, SWT.NULL, 1); tableColumnContext.setResizable(true); tableColumnContext.setText(Util.translateString(RESOURCE_BUNDLE, "tableColumnContext")); //$NON-NLS-1$ tableColumnContext.pack(); tableColumnContext.setWidth(200); final TableColumn tableColumnCommand = new TableColumn( tableBindingsForTriggerSequence, SWT.NULL, 2); tableColumnCommand.setResizable(true); tableColumnCommand.setText(Util.translateString(RESOURCE_BUNDLE, "tableColumnCommand")); //$NON-NLS-1$ tableColumnCommand.pack(); tableColumnCommand.setWidth(300); tableBindingsForTriggerSequence.addMouseListener(new MouseAdapter() { @Override public void mouseDoubleClick(MouseEvent mouseEvent) { update(); } }); tableBindingsForTriggerSequence .addSelectionListener(widgetSelectedAdapter(selectionEvent -> selectedTableBindingsForTriggerSequence())); final Composite compositeContext = new Composite(composite, SWT.NULL); gridLayout = new GridLayout(); gridLayout.numColumns = 3; compositeContext.setLayout(gridLayout); gridData = new GridData(GridData.FILL_HORIZONTAL); compositeContext.setLayoutData(gridData); final Label labelContext = new Label(compositeContext, SWT.LEFT); labelContext.setText(Util.translateString(RESOURCE_BUNDLE, "labelContext")); //$NON-NLS-1$ comboContext = new Combo(compositeContext, SWT.READ_ONLY); gridData = new GridData(); gridData.widthHint = 250; comboContext.setLayoutData(gridData); comboContext.setVisibleItemCount(ITEMS_TO_SHOW); comboContext.addSelectionListener(widgetSelectedAdapter(e -> update())); labelContextExtends = new Label(compositeContext, SWT.LEFT); gridData = new GridData(GridData.FILL_HORIZONTAL); labelContextExtends.setLayoutData(gridData); final Composite compositeButton = new Composite(composite, SWT.NULL); gridLayout = new GridLayout(); gridLayout.marginHeight = 20; gridLayout.marginWidth = 0; gridLayout.numColumns = 3; compositeButton.setLayout(gridLayout); gridData = new GridData(); compositeButton.setLayoutData(gridData); buttonAdd = new Button(compositeButton, SWT.CENTER | SWT.PUSH); gridData = new GridData(); int widthHint = convertHorizontalDLUsToPixels(IDialogConstants.BUTTON_WIDTH); buttonAdd.setText(Util.translateString(RESOURCE_BUNDLE, "buttonAdd")); //$NON-NLS-1$ gridData.widthHint = Math.max(widthHint, buttonAdd.computeSize( SWT.DEFAULT, SWT.DEFAULT, true).x) + 5; buttonAdd.setLayoutData(gridData); buttonAdd.addSelectionListener(widgetSelectedAdapter(selectionEvent -> selectedButtonAdd())); buttonRemove = new Button(compositeButton, SWT.CENTER | SWT.PUSH); gridData = new GridData(); widthHint = convertHorizontalDLUsToPixels(IDialogConstants.BUTTON_WIDTH); buttonRemove.setText(Util.translateString(RESOURCE_BUNDLE, "buttonRemove")); //$NON-NLS-1$ gridData.widthHint = Math.max(widthHint, buttonRemove.computeSize( SWT.DEFAULT, SWT.DEFAULT, true).x) + 5; buttonRemove.setLayoutData(gridData); buttonRemove.addSelectionListener(widgetSelectedAdapter(selectionEvent -> selectedButtonRemove())); buttonRestore = new Button(compositeButton, SWT.CENTER | SWT.PUSH); gridData = new GridData(); widthHint = convertHorizontalDLUsToPixels(IDialogConstants.BUTTON_WIDTH); buttonRestore.setText(Util.translateString(RESOURCE_BUNDLE, "buttonRestore")); //$NON-NLS-1$ gridData.widthHint = Math.max(widthHint, buttonRestore.computeSize( SWT.DEFAULT, SWT.DEFAULT, true).x) + 5; buttonRestore.setLayoutData(gridData); buttonRestore.addSelectionListener(widgetSelectedAdapter(selectionEvent -> selectedButtonRestore())); return composite; } /** * Creates a tab on the main page for displaying an uneditable list of the * current key bindings. This is intended as a discovery tool for new users. * It shows all of the key bindings for the current key configuration, * platform and locale. * * @param parent * The tab folder in which the tab should be created; must not be * <code>null</code>. * @return The newly created composite containing all of the controls; never * <code>null</code>. * @since 3.1 */ private final Composite createViewTab(final TabFolder parent) { GridData gridData = null; int widthHint; // Create the composite for the tab. final Composite composite = new Composite(parent, SWT.NONE); composite.setLayoutData(new GridData(GridData.FILL_BOTH)); composite.setLayout(new GridLayout()); // Place a table inside the tab. tableBindings = new Table(composite, SWT.BORDER | SWT.FULL_SELECTION | SWT.H_SCROLL | SWT.V_SCROLL); tableBindings.setHeaderVisible(true); gridData = new GridData(GridData.FILL_BOTH); gridData.heightHint = 400; gridData.horizontalSpan = 2; tableBindings.setLayoutData(gridData); final TableColumn tableColumnCategory = new TableColumn(tableBindings, SWT.NONE, VIEW_CATEGORY_COLUMN_INDEX); tableColumnCategory .setText(SORTED_COLUMN_NAMES[VIEW_CATEGORY_COLUMN_INDEX]); tableColumnCategory .addSelectionListener(new SortOrderSelectionListener( VIEW_CATEGORY_COLUMN_INDEX)); final TableColumn tableColumnCommand = new TableColumn(tableBindings, SWT.NONE, VIEW_COMMAND_COLUMN_INDEX); tableColumnCommand .setText(UNSORTED_COLUMN_NAMES[VIEW_COMMAND_COLUMN_INDEX]); tableColumnCommand.addSelectionListener(new SortOrderSelectionListener( VIEW_COMMAND_COLUMN_INDEX)); final TableColumn tableColumnKeySequence = new TableColumn( tableBindings, SWT.NONE, VIEW_KEY_SEQUENCE_COLUMN_INDEX); tableColumnKeySequence .setText(UNSORTED_COLUMN_NAMES[VIEW_KEY_SEQUENCE_COLUMN_INDEX]); tableColumnKeySequence .addSelectionListener(new SortOrderSelectionListener( VIEW_KEY_SEQUENCE_COLUMN_INDEX)); final TableColumn tableColumnContext = new TableColumn(tableBindings, SWT.NONE, VIEW_CONTEXT_COLUMN_INDEX); tableColumnContext .setText(UNSORTED_COLUMN_NAMES[VIEW_CONTEXT_COLUMN_INDEX]); tableColumnContext.addSelectionListener(new SortOrderSelectionListener( VIEW_CONTEXT_COLUMN_INDEX)); tableBindings.addSelectionListener(widgetDefaultSelectedAdapter(e -> selectedTableKeyBindings())); // A composite for the buttons. final Composite buttonBar = new Composite(composite, SWT.NONE); buttonBar.setLayout(new GridLayout(2, false)); gridData = new GridData(); gridData.horizontalAlignment = GridData.END; buttonBar.setLayoutData(gridData); // A button for editing the current selection. final Button editButton = new Button(buttonBar, SWT.PUSH); gridData = new GridData(); widthHint = convertHorizontalDLUsToPixels(IDialogConstants.BUTTON_WIDTH); editButton.setText(Util.translateString(RESOURCE_BUNDLE, "buttonEdit")); //$NON-NLS-1$ gridData.widthHint = Math.max(widthHint, editButton.computeSize( SWT.DEFAULT, SWT.DEFAULT, true).x) + 5; editButton.setLayoutData(gridData); editButton.addSelectionListener(new SelectionListener() { @Override public final void widgetDefaultSelected(final SelectionEvent event) { selectedTableKeyBindings(); } @Override public void widgetSelected(SelectionEvent e) { widgetDefaultSelected(e); } }); // A button for exporting the contents to a file. final Button buttonExport = new Button(buttonBar, SWT.PUSH); gridData = new GridData(); widthHint = convertHorizontalDLUsToPixels(IDialogConstants.BUTTON_WIDTH); buttonExport.setText(Util.translateString(RESOURCE_BUNDLE, "buttonExport")); //$NON-NLS-1$ gridData.widthHint = Math.max(widthHint, buttonExport.computeSize( SWT.DEFAULT, SWT.DEFAULT, true).x) + 5; buttonExport.setLayoutData(gridData); buttonExport.addSelectionListener(new SelectionListener() { @Override public final void widgetDefaultSelected(final SelectionEvent event) { selectedButtonExport(); } @Override public void widgetSelected(SelectionEvent e) { widgetDefaultSelected(e); } }); return composite; } @Override protected IPreferenceStore doGetPreferenceStore() { return PrefUtil.getInternalPreferenceStore(); } /** * Allows the user to change the key bindings for a particular command. * Switches the tab to the modify tab, and then selects the category and * command that corresponds with the given command name. It then selects the * given key sequence and gives focus to the key sequence text widget. * * @param binding * The binding to be edited; if <code>null</code>, then just * switch to the modify tab. If the <code>binding</code> does * not correspond to anything in the keys preference page, then * this also just switches to the modify tab. * @since 3.1 */ public final void editBinding(final Binding binding) { // Switch to the modify tab. tabFolder.setSelection(TAB_INDEX_MODIFY); // If there is no command name, stop here. if (binding == null) { return; } /* * Get the corresponding category and command names. If either is * undefined, then we can just stop now. We won't be able to find their * name. */ final ParameterizedCommand command = binding.getParameterizedCommand(); String categoryName = null; String commandName = null; try { categoryName = command.getCommand().getCategory().getName(); commandName = command.getName(); } catch (final NotDefinedException e) { return; // no name } // Update the category combo box. final String[] categoryNames = comboCategory.getItems(); int i = 0; for (; i < categoryNames.length; i++) { if (categoryName.equals(categoryNames[i])) { break; } } if (i >= comboCategory.getItemCount()) { // Couldn't find the category, so abort. return; } comboCategory.select(i); // Update the commands combo box. updateComboCommand(); // Update the command combo box. final String[] commandNames = comboCommand.getItems(); int j = 0; for (; j < commandNames.length; j++) { if (commandName.equals(commandNames[j])) { if (comboCommand.getSelectionIndex() != j) { comboCommand.select(j); } break; } } if (j >= comboCommand.getItemCount()) { // Couldn't find the command, so just select the first and then stop if (comboCommand.getSelectionIndex() != 0) { comboCommand.select(0); } update(); return; } /* * Update and validate the state of the modify tab in response to these * selection changes. */ update(); // Select the right key binding, if possible. final TableItem[] items = tableBindingsForCommand.getItems(); int k = 0; for (; k < items.length; k++) { final String currentKeySequence = items[k].getText(2); if (binding.getTriggerSequence().format() .equals(currentKeySequence)) { break; } } if (k < tableBindingsForCommand.getItemCount()) { tableBindingsForCommand.select(k); tableBindingsForCommand.notifyListeners(SWT.Selection, null); textTriggerSequence.setFocus(); } } /** * Returns the identifier for the currently selected category. * * @return The selected category; <code>null</code> if none. */ private final String getCategoryId() { return !commandIdsByCategoryId.containsKey(null) || comboCategory.getSelectionIndex() > 0 ? (String) categoryIdsByUniqueName .get(comboCategory.getText()) : null; } /** * Returns the identifier for the currently selected context. * * @return The selected context; <code>null</code> if none. */ private final String getContextId() { return comboContext.getSelectionIndex() >= 0 ? (String) contextIdsByUniqueName .get(comboContext.getText()) : null; } /** * Returns the current trigger sequence. * * @return The trigger sequence; may be empty, but never <code>null</code>. */ private final KeySequence getKeySequence() { return textTriggerSequenceManager.getKeySequence(); } /** * Returns the currently-selected fully-parameterized command. * * @return The selected fully-parameterized command; <code>null</code> if * none. */ private final ParameterizedCommand getParameterizedCommand() { final int selectionIndex = comboCommand.getSelectionIndex(); if ((selectionIndex >= 0) && (commands != null) && (selectionIndex < commands.length)) { return commands[selectionIndex]; } return null; } /** * Returns the identifier for the currently selected scheme. * * @return The selected scheme; <code>null</code> if none. */ private final String getSchemeId() { return comboScheme.getSelectionIndex() >= 0 ? (String) schemeIdsByUniqueName .get(comboScheme.getText()) : null; } @Override public final void init(final IWorkbench workbench) { activityManager = workbench.getActivitySupport().getActivityManager(); bindingService = workbench.getService(IBindingService.class); commandService = workbench.getService(ICommandService.class); contextService = workbench.getService(IContextService.class); } /** * Checks whether the activity manager knows anything about this command * identifier. If the activity manager is currently filtering this command, * then it does not appear in the user interface. * * @param command * The command which should be checked against the activities; * must not be <code>null</code>. * @return <code>true</code> if the command identifier is not filtered; * <code>false</code> if it is */ private final boolean isActive(final Command command) { return activityManager.getIdentifier(command.getId()).isEnabled(); } /** * Logs the given exception, and opens an error dialog saying that something * went wrong. The exception is assumed to have something to do with the * preference store. * * @param exception * The exception to be logged; must not be <code>null</code>. */ private final void logPreferenceStoreException(final Throwable exception) { final String message = Util.translateString(RESOURCE_BUNDLE, "PreferenceStoreError.Message"); //$NON-NLS-1$ String exceptionMessage = exception.getMessage(); if (exceptionMessage == null) { exceptionMessage = message; } final IStatus status = new Status(IStatus.ERROR, WorkbenchPlugin.PI_WORKBENCH, 0, exceptionMessage, exception); WorkbenchPlugin.log(message, status); StatusUtil.handleStatus(message, exception, StatusManager.SHOW); } @Override public final boolean performCancel() { // Save the selected tab for future reference. persistSelectedTab(); return super.performCancel(); } @Override protected final void performDefaults() { // Ask the user to confirm final String title = Util.translateString(RESOURCE_BUNDLE, "restoreDefaultsMessageBoxText"); //$NON-NLS-1$ final String message = Util.translateString(RESOURCE_BUNDLE, "restoreDefaultsMessageBoxMessage"); //$NON-NLS-1$ final boolean confirmed = MessageDialog.open(MessageDialog.CONFIRM, getShell(), title, message, SWT.SHEET); if (confirmed) { // Fix the scheme in the local changes. final String defaultSchemeId = bindingService.getDefaultSchemeId(); final Scheme defaultScheme = localChangeManager .getScheme(defaultSchemeId); try { localChangeManager.setActiveScheme(defaultScheme); } catch (final NotDefinedException e) { // At least we tried.... } // Fix the bindings in the local changes. final Binding[] currentBindings = localChangeManager.getBindings(); final int currentBindingsLength = currentBindings.length; final Set trimmedBindings = new HashSet(); for (int i = 0; i < currentBindingsLength; i++) { final Binding binding = currentBindings[i]; if (binding.getType() != Binding.USER) { trimmedBindings.add(binding); } } final Binding[] trimmedBindingArray = (Binding[]) trimmedBindings .toArray(new Binding[trimmedBindings.size()]); localChangeManager.setBindings(trimmedBindingArray); // Apply the changes. try { bindingService.savePreferences(defaultScheme, trimmedBindingArray); } catch (final IOException e) { logPreferenceStoreException(e); } } setScheme(localChangeManager.getActiveScheme()); // update the scheme update(true); super.performDefaults(); } @Override public final boolean performOk() { // Save the preferences. try { bindingService.savePreferences( localChangeManager.getActiveScheme(), localChangeManager .getBindings()); } catch (final IOException e) { logPreferenceStoreException(e); } // Save the selected tab for future reference. persistSelectedTab(); return super.performOk(); } /** * Remembers the currently selected tab for when the preference page next * opens. */ private final void persistSelectedTab() { final IPreferenceStore store = getPreferenceStore(); store.setValue(IPreferenceConstants.KEYS_PREFERENCE_SELECTED_TAB, tabFolder.getSelectionIndex()); } /** * Handles the selection event on the add button. This removes all * user-defined bindings matching the given key sequence, scheme and * context. It then adds a new binding with the current selections. */ private final void selectedButtonAdd() { final ParameterizedCommand command = getParameterizedCommand(); final String contextId = getContextId(); final String schemeId = getSchemeId(); final KeySequence keySequence = getKeySequence(); localChangeManager.removeBindings(keySequence, schemeId, contextId, null, null, null, Binding.USER); localChangeManager.addBinding(new KeyBinding(keySequence, command, schemeId, contextId, null, null, null, Binding.USER)); update(true); } /** * Provides a facility for exporting the viewable list of key bindings to a * file. Currently, this only supports exporting to a list of * comma-separated values. The user is prompted for which file should * receive our bounty. * * @since 3.1 */ private final void selectedButtonExport() { final FileDialog fileDialog = new FileDialog(getShell(), SWT.SAVE | SWT.SHEET); fileDialog.setFilterExtensions(new String[] { "*.csv" }); //$NON-NLS-1$ fileDialog.setFilterNames(new String[] { Util.translateString( RESOURCE_BUNDLE, "csvFilterName") }); //$NON-NLS-1$ final String filePath = fileDialog.open(); if (filePath == null) { return; } final SafeRunnable runnable = new SafeRunnable() { @Override public final void run() throws IOException { Writer fileWriter = null; try { fileWriter = new BufferedWriter(new FileWriter(filePath)); final TableItem[] items = tableBindings.getItems(); final int numColumns = tableBindings.getColumnCount(); for (final TableItem item : items) { for (int j = 0; j < numColumns; j++) { String buf = Util.replaceAll(item.getText(j), "\"", //$NON-NLS-1$ "\"\""); //$NON-NLS-1$ fileWriter.write("\"" + buf + "\""); //$NON-NLS-1$//$NON-NLS-2$ if (j < numColumns - 1) { fileWriter.write(','); } } fileWriter.write(System.getProperty("line.separator")); //$NON-NLS-1$ } } finally { if (fileWriter != null) { try { fileWriter.close(); } catch (final IOException e) { // At least I tried. } } } } }; SafeRunner.run(runnable); } /** * Handles the selection event on the remove button. This removes all * user-defined bindings matching the given key sequence, scheme and * context. It then adds a new deletion binding for the selected trigger * sequence. */ private final void selectedButtonRemove() { final String contextId = getContextId(); final String schemeId = getSchemeId(); final KeySequence keySequence = getKeySequence(); localChangeManager.removeBindings(keySequence, schemeId, contextId, null, null, null, Binding.USER); localChangeManager.addBinding(new KeyBinding(keySequence, null, schemeId, contextId, null, null, null, Binding.USER)); update(true); } /** * Handles the selection event on the restore button. This removes all * user-defined bindings matching the given key sequence, scheme and * context. */ private final void selectedButtonRestore() { String contextId = getContextId(); String schemeId = getSchemeId(); KeySequence keySequence = getKeySequence(); localChangeManager.removeBindings(keySequence, schemeId, contextId, null, null, null, Binding.USER); update(true); } /** * Updates the local managers active scheme, and then updates the interface. */ private final void selectedComboScheme() { final String activeSchemeId = getSchemeId(); final Scheme activeScheme = localChangeManager .getScheme(activeSchemeId); try { localChangeManager.setActiveScheme(activeScheme); } catch (final NotDefinedException e) { // Oh, well. } update(true); } /** * Handles the selection event on the table containing the bindings for a * particular command. This updates the context and trigger sequence based * on the selected binding. */ private final void selectedTableBindingsForCommand() { final int selection = tableBindingsForCommand.getSelectionIndex(); if ((selection >= 0) && (selection < tableBindingsForCommand.getItemCount())) { final TableItem item = tableBindingsForCommand.getItem(selection); final KeyBinding binding = (KeyBinding) item.getData(ITEM_DATA_KEY); setContextId(binding.getContextId()); setKeySequence(binding.getKeySequence()); } update(); } /** * Handles the selection event on the table containing the bindings for a * particular trigger sequence. This updates the context based on the * selected binding. */ private final void selectedTableBindingsForTriggerSequence() { final int selection = tableBindingsForTriggerSequence .getSelectionIndex(); if ((selection >= 0) && (selection < tableBindingsForTriggerSequence.getItemCount())) { final TableItem item = tableBindingsForTriggerSequence .getItem(selection); final Binding binding = (Binding) item.getData(ITEM_DATA_KEY); setContextId(binding.getContextId()); } update(); } /** * Responds to some kind of trigger on the View tab by taking the current * selection on the key bindings table and selecting the appropriate items * in the Modify tab. * * @since 3.1 */ private final void selectedTableKeyBindings() { final int selectionIndex = tableBindings.getSelectionIndex(); if (selectionIndex != -1) { final TableItem item = tableBindings.getItem(selectionIndex); final Binding binding = (Binding) item.getData(BINDING_KEY); editBinding(binding); } else { editBinding(null); } } /** * Changes the selected context name in the context combo box. The context * selected is either the one matching the identifier provided (if * possible), or the default context identifier. If no matching name can be * found in the combo, then the first item is selected. * * @param contextId * The context identifier for the context to be selected in the * combo box; may be <code>null</code>. */ private final void setContextId(final String contextId) { // Clear the current selection. comboContext.clearSelection(); comboContext.deselectAll(); // Figure out which name to look for. String contextName = (String) contextUniqueNamesById.get(contextId); if (contextName == null) { contextName = (String) contextUniqueNamesById .get(IContextIds.CONTEXT_ID_WINDOW); } if (contextName == null) { contextName = Util.ZERO_LENGTH_STRING; } // Scan the list for the selection we're looking for. final String[] items = comboContext.getItems(); boolean found = false; for (int i = 0; i < items.length; i++) { if (contextName.equals(items[i])) { comboContext.select(i); found = true; break; } } // If we didn't find an item, then set the first item as selected. if ((!found) && (items.length > 0)) { comboContext.select(0); } } /** * Sets the current trigger sequence. * * @param keySequence * The trigger sequence; may be <code>null</code>. */ private final void setKeySequence(final KeySequence keySequence) { textTriggerSequenceManager.setKeySequence(keySequence); } /** * Changes the selection in the command combo box. * * @param command * The fully-parameterized command to select; may be * <code>null</code>. */ private final void setParameterizedCommand( final ParameterizedCommand command) { int i = 0; if (commands != null) { final int commandCount = commands.length; for (; i < commandCount; i++) { if (commands[i].equals(command)) { if ((comboCommand.getSelectionIndex() != i) && (i < comboCommand.getItemCount())) { comboCommand.select(i); } break; } } if ((i >= comboCommand.getItemCount()) && (comboCommand.getSelectionIndex() != 0)) { comboCommand.select(0); } } } /** * Sets the currently selected scheme * * @param scheme * The scheme to select; may be <code>null</code>. */ private final void setScheme(final Scheme scheme) { comboScheme.clearSelection(); comboScheme.deselectAll(); final String schemeUniqueName = (String) schemeUniqueNamesById .get(scheme.getId()); if (schemeUniqueName != null) { final String items[] = comboScheme.getItems(); for (int i = 0; i < items.length; i++) { if (schemeUniqueName.equals(items[i])) { comboScheme.select(i); break; } } } } /** * Builds the internal look-up tables before allowing the page to become * visible. */ @Override public final void setVisible(final boolean visible) { if (visible == true) { Map contextsByName = new HashMap(); for (Iterator iterator = contextService.getDefinedContextIds() .iterator(); iterator.hasNext();) { Context context = contextService.getContext((String) iterator .next()); try { String name = context.getName(); Collection contexts = (Collection) contextsByName.get(name); if (contexts == null) { contexts = new HashSet(); contextsByName.put(name, contexts); } contexts.add(context); } catch (final NotDefinedException e) { // Do nothing. } } Map commandsByName = new HashMap(); for (Iterator iterator = commandService.getDefinedCommandIds() .iterator(); iterator.hasNext();) { Command command = commandService.getCommand((String) iterator .next()); if (!isActive(command)) { continue; } try { String name = command.getName(); Collection commands = (Collection) commandsByName.get(name); if (commands == null) { commands = new HashSet(); commandsByName.put(name, commands); } commands.add(command); } catch (NotDefinedException eNotDefined) { // Do nothing } } // moved here to allow us to remove any empty categories commandIdsByCategoryId = new HashMap(); for (Iterator iterator = commandService.getDefinedCommandIds() .iterator(); iterator.hasNext();) { final Command command = commandService .getCommand((String) iterator.next()); if (!isActive(command)) { continue; } try { String categoryId = command.getCategory().getId(); Collection commandIds = (Collection) commandIdsByCategoryId .get(categoryId); if (commandIds == null) { commandIds = new HashSet(); commandIdsByCategoryId.put(categoryId, commandIds); } commandIds.add(command.getId()); } catch (NotDefinedException eNotDefined) { // Do nothing } } Map categoriesByName = new HashMap(); for (Iterator iterator = commandService.getDefinedCategoryIds() .iterator(); iterator.hasNext();) { Category category = commandService .getCategory((String) iterator.next()); try { if (commandIdsByCategoryId.containsKey(category.getId())) { String name = category.getName(); Collection categories = (Collection) categoriesByName .get(name); if (categories == null) { categories = new HashSet(); categoriesByName.put(name, categories); } categories.add(category); } } catch (NotDefinedException eNotDefined) { // Do nothing } } Map schemesByName = new HashMap(); final Scheme[] definedSchemes = bindingService.getDefinedSchemes(); for (final Scheme scheme : definedSchemes) { try { String name = scheme.getName(); Collection schemes = (Collection) schemesByName.get(name); if (schemes == null) { schemes = new HashSet(); schemesByName.put(name, schemes); } schemes.add(scheme); } catch (final NotDefinedException e) { // Do nothing. } } contextIdsByUniqueName = new HashMap(); contextUniqueNamesById = new HashMap(); for (Iterator iterator = contextsByName.entrySet().iterator(); iterator .hasNext();) { Map.Entry entry = (Map.Entry) iterator.next(); String name = (String) entry.getKey(); Set contexts = (Set) entry.getValue(); Iterator iterator2 = contexts.iterator(); if (contexts.size() == 1) { Context context = (Context) iterator2.next(); contextIdsByUniqueName.put(name, context.getId()); contextUniqueNamesById.put(context.getId(), name); } else { while (iterator2.hasNext()) { Context context = (Context) iterator2.next(); String uniqueName = MessageFormat.format( Util.translateString(RESOURCE_BUNDLE, "uniqueName"), //$NON-NLS-1$ name, context.getId()); contextIdsByUniqueName.put(uniqueName, context.getId()); contextUniqueNamesById.put(context.getId(), uniqueName); } } } categoryIdsByUniqueName = new HashMap(); categoryUniqueNamesById = new HashMap(); for (Iterator iterator = categoriesByName.entrySet().iterator(); iterator .hasNext();) { Map.Entry entry = (Map.Entry) iterator.next(); String name = (String) entry.getKey(); Set categories = (Set) entry.getValue(); Iterator iterator2 = categories.iterator(); if (categories.size() == 1) { Category category = (Category) iterator2.next(); categoryIdsByUniqueName.put(name, category.getId()); categoryUniqueNamesById.put(category.getId(), name); } else { while (iterator2.hasNext()) { Category category = (Category) iterator2.next(); String uniqueName = MessageFormat.format( Util.translateString(RESOURCE_BUNDLE, "uniqueName"), new Object[] { name, //$NON-NLS-1$ category.getId() }); categoryIdsByUniqueName.put(uniqueName, category .getId()); categoryUniqueNamesById.put(category.getId(), uniqueName); } } } schemeIdsByUniqueName = new HashMap(); schemeUniqueNamesById = new HashMap(); for (Iterator iterator = schemesByName.entrySet().iterator(); iterator .hasNext();) { Map.Entry entry = (Map.Entry) iterator.next(); String name = (String) entry.getKey(); Set keyConfigurations = (Set) entry.getValue(); Iterator iterator2 = keyConfigurations.iterator(); if (keyConfigurations.size() == 1) { Scheme scheme = (Scheme) iterator2.next(); schemeIdsByUniqueName.put(name, scheme.getId()); schemeUniqueNamesById.put(scheme.getId(), name); } else { while (iterator2.hasNext()) { Scheme scheme = (Scheme) iterator2.next(); String uniqueName = MessageFormat.format( Util.translateString(RESOURCE_BUNDLE, "uniqueName"), new Object[] { name, //$NON-NLS-1$ scheme.getId() }); schemeIdsByUniqueName.put(uniqueName, scheme.getId()); schemeUniqueNamesById.put(scheme.getId(), uniqueName); } } } Scheme activeScheme = bindingService.getActiveScheme(); // Make an internal copy of the binding manager, for local changes. try { for (final Scheme scheme : definedSchemes) { final Scheme copy = localChangeManager.getScheme(scheme .getId()); copy.define(scheme.getName(), scheme.getDescription(), scheme.getParentId()); } localChangeManager.setActiveScheme(bindingService .getActiveScheme()); } catch (final NotDefinedException e) { throw new Error( "There is a programmer error in the keys preference page"); //$NON-NLS-1$ } localChangeManager.setLocale(bindingService.getLocale()); localChangeManager.setPlatform(bindingService.getPlatform()); localChangeManager.setBindings(bindingService.getBindings()); // Populate the category combo box. List categoryNames = new ArrayList(categoryIdsByUniqueName.keySet()); Collections.sort(categoryNames, Collator.getInstance()); if (commandIdsByCategoryId.containsKey(null)) { categoryNames.add(0, Util.translateString(RESOURCE_BUNDLE, "other")); //$NON-NLS-1$ } comboCategory.setItems((String[]) categoryNames .toArray(new String[categoryNames.size()])); comboCategory.clearSelection(); comboCategory.deselectAll(); if (commandIdsByCategoryId.containsKey(null) || !categoryNames.isEmpty()) { comboCategory.select(0); } // Populate the scheme combo box. List schemeNames = new ArrayList(schemeIdsByUniqueName.keySet()); Collections.sort(schemeNames, Collator.getInstance()); comboScheme.setItems((String[]) schemeNames .toArray(new String[schemeNames.size()])); setScheme(activeScheme); // Update the entire page. update(true); } super.setVisible(visible); } /** * Updates the entire preference page -- except the view tab -- based on * current selection sate. This preference page is written so that * everything can be made consistent simply by inspecting the state of its * widgets. A change is triggered by the user, and an event is fired. The * event triggers an update. It is possible for extra work to be done by * this page before calling update. */ private final void update() { update(false); } /** * Updates the entire preference page based on current changes. This * preference page is written so that everything can be made consistent * simply by inspecting the state of its widgets. A change is triggered by * the user, and an event is fired. The event triggers an update. It is * possible for extra work to be done by this page before calling update. * * @param updateViewTab * Whether the view tab should be updated as well. */ private final void update(final boolean updateViewTab) { if (updateViewTab) { updateViewTab(); } updateComboCommand(); updateComboContext(); final TriggerSequence triggerSequence = getKeySequence(); updateTableBindingsForTriggerSequence(triggerSequence); final ParameterizedCommand command = getParameterizedCommand(); updateTableBindingsForCommand(command); final String contextId = getContextId(); updateSelection(tableBindingsForTriggerSequence, contextId, triggerSequence); updateSelection(tableBindingsForCommand, contextId, triggerSequence); updateLabelSchemeExtends(); updateLabelContextExtends(); updateEnabled(triggerSequence, command); } /** * Updates the contents of the commands combo box, based on the current * selection in the category combo box. */ private final void updateComboCommand() { // Remember the current selection, so we can restore it later. final ParameterizedCommand command = getParameterizedCommand(); // Figure out where command identifiers apply to the selected category. final String categoryId = getCategoryId(); Set commandIds = (Set) commandIdsByCategoryId.get(categoryId); if (commandIds==null) { commandIds = Collections.EMPTY_SET; } /* * Generate an array of parameterized commands based on these * identifiers. The parameterized commands will be sorted based on their * names. */ List commands = new ArrayList(); final Iterator commandIdItr = commandIds.iterator(); while (commandIdItr.hasNext()) { final String currentCommandId = (String) commandIdItr.next(); final Command currentCommand = commandService .getCommand(currentCommandId); try { commands.addAll(ParameterizedCommand .generateCombinations(currentCommand)); } catch (final NotDefinedException e) { // It is safe to just ignore undefined commands. } } // sort the commands with a collator, so they appear in the // combo correctly commands = sortParameterizedCommands(commands); final int commandCount = commands.size(); this.commands = (ParameterizedCommand[]) commands .toArray(new ParameterizedCommand[commandCount]); /* * Generate an array of command names based on this array of * parameterized commands. */ final String[] commandNames = new String[commandCount]; for (int i = 0; i < commandCount; i++) { try { commandNames[i] = this.commands[i].getName(); } catch (final NotDefinedException e) { throw new Error( "Concurrent modification of the command's defined state"); //$NON-NLS-1$ } } /* * Copy the command names into the combo box, but only if they've * changed. We do this to try to avoid unnecessary calls out to the * operating system, as well as to defend against bugs in SWT's event * mechanism. */ final String[] currentItems = comboCommand.getItems(); if (!Arrays.equals(currentItems, commandNames)) { comboCommand.setItems(commandNames); } // Try to restore the selection. setParameterizedCommand(command); /* * Just to be extra careful, make sure that we have a selection at this * point. This line could probably be removed, but it makes the code a * bit more robust. */ if ((comboCommand.getSelectionIndex() == -1) && (commandCount > 0)) { comboCommand.select(0); } } /** * Sort the commands using the correct language. * @param commands the List of ParameterizedCommands * @return The sorted List */ private List sortParameterizedCommands(List commands) { final Collator collator = Collator.getInstance(); // this comparator is based on the ParameterizedCommands#compareTo(*) // method, but uses the collator. Comparator comparator = (o1, o2) -> { String name1 = null; String name2 = null; try { name1 = ((ParameterizedCommand) o1).getName(); } catch (NotDefinedException e1) { return -1; } try { name2 = ((ParameterizedCommand) o2).getName(); } catch (NotDefinedException e2) { return 1; } int rc = collator.compare(name1, name2); if (rc != 0) { return rc; } String id1 = ((ParameterizedCommand) o1).getId(); String id2 = ((ParameterizedCommand) o2).getId(); return collator.compare(id1, id2); }; Collections.sort(commands, comparator); return commands; } /** * Updates the contents of the context combo box, as well as its selection. */ private final void updateComboContext() { final String contextId = getContextId(); final Map contextIdsByName = new HashMap(contextIdsByUniqueName); final List contextNames = new ArrayList(contextIdsByName.keySet()); Collections.sort(contextNames, Collator.getInstance()); comboContext.setItems((String[]) contextNames .toArray(new String[contextNames.size()])); setContextId(contextId); if (comboContext.getSelectionIndex() == -1 && !contextNames.isEmpty()) { comboContext.select(0); } } /** * Updates the enabled state of the various widgets on this page. The * decision is based on the current trigger sequence and the currently * selected command. * * @param triggerSequence * The current trigger sequence; may be empty, but never * <code>null</code>. * @param command * The currently selected command, if any; <code>null</code> * otherwise. */ private final void updateEnabled(final TriggerSequence triggerSequence, final ParameterizedCommand command) { final boolean commandSelected = command != null; labelBindingsForCommand.setEnabled(commandSelected); tableBindingsForCommand.setEnabled(commandSelected); final boolean triggerSequenceSelected = !triggerSequence.isEmpty(); labelBindingsForTriggerSequence.setEnabled(triggerSequenceSelected); tableBindingsForTriggerSequence.setEnabled(triggerSequenceSelected); /* * TODO Do some better button enablement. */ final boolean buttonsEnabled = commandSelected && triggerSequenceSelected; buttonAdd.setEnabled(buttonsEnabled); buttonRemove.setEnabled(buttonsEnabled); buttonRestore.setEnabled(buttonsEnabled); } /** * Updates the label next to the context that says "extends" if the context * is a child of another context. If the context is not a child of another * context, then the label is simply blank. */ private final void updateLabelContextExtends() { final String contextId = getContextId(); if (contextId != null) { final Context context = contextService.getContext(getContextId()); if (context.isDefined()) { try { final String parentId = context.getParentId(); if (parentId != null) { final String name = (String) contextUniqueNamesById .get(parentId); if (name != null) { labelContextExtends.setText(MessageFormat.format( Util.translateString(RESOURCE_BUNDLE, "extends"), //$NON-NLS-1$ name)); return; } } } catch (final NotDefinedException e) { // Do nothing } } } labelContextExtends.setText(Util.ZERO_LENGTH_STRING); } /** * Updates the label next to the scheme that says "extends" if the scheme is * a child of another scheme. If the scheme is not a child of another * scheme, then the label is simply blank. */ private final void updateLabelSchemeExtends() { final String schemeId = getSchemeId(); if (schemeId != null) { final Scheme scheme = bindingService.getScheme(schemeId); try { final String name = (String) schemeUniqueNamesById.get(scheme .getParentId()); if (name != null) { labelSchemeExtends.setText(MessageFormat.format(Util .translateString(RESOURCE_BUNDLE, "extends"), //$NON-NLS-1$ name)); return; } } catch (final NotDefinedException e) { // Do nothing } } labelSchemeExtends.setText(Util.ZERO_LENGTH_STRING); } /** * Tries to select the correct entry in table based on the currently * selected context and trigger sequence. If the table hasn't really * changed, then this method is essentially trying to restore the selection. * If it has changed, then it is trying to select the most entry based on * the context. * * @param table * The table to be changed; must not be <code>null</code>. * @param contextId * The currently selected context; should not be * <code>null</code>. * @param triggerSequence * The current trigger sequence; should not be <code>null</code>. */ private final void updateSelection(final Table table, final String contextId, final TriggerSequence triggerSequence) { if (table.getSelectionCount() > 1) { table.deselectAll(); } final TableItem[] items = table.getItems(); int selection = -1; for (int i = 0; i < items.length; i++) { final Binding binding = (Binding) items[i].getData(ITEM_DATA_KEY); if ((Util.equals(contextId, binding.getContextId())) && (Util.equals(triggerSequence, binding .getTriggerSequence()))) { selection = i; break; } } if (selection != -1) { table.select(selection); } } /** * Updates the contents of the table showing the bindings for the currently * selected command. The selection is destroyed by this process. * * @param parameterizedCommand * The currently selected fully-parameterized command; may be * <code>null</code>. */ private final void updateTableBindingsForCommand( final ParameterizedCommand parameterizedCommand) { // Clear the table of existing items. tableBindingsForCommand.removeAll(); // Add each of the bindings, if the command identifier matches. final Collection bindings = localChangeManager .getActiveBindingsDisregardingContextFlat(); final Iterator bindingItr = bindings.iterator(); while (bindingItr.hasNext()) { final Binding binding = (Binding) bindingItr.next(); if (!Util.equals(parameterizedCommand, binding .getParameterizedCommand())) { continue; // binding does not match } final TableItem tableItem = new TableItem(tableBindingsForCommand, SWT.NULL); tableItem.setData(ITEM_DATA_KEY, binding); /* * Set the associated image based on the type of binding. Either it * is a user binding or a system binding. * * TODO Identify more image types. */ if (binding.getType() == Binding.SYSTEM) { tableItem.setImage(0, IMAGE_BLANK); } else { tableItem.setImage(0, IMAGE_CHANGE); } String contextName = (String) contextUniqueNamesById.get(binding .getContextId()); if (contextName == null) { contextName = Util.ZERO_LENGTH_STRING; } tableItem.setText(1, contextName); tableItem.setText(2, binding.getTriggerSequence().format()); } } /** * Updates the contents of the table showing the bindings for the current * trigger sequence. The selection is destroyed by this process. * * @param triggerSequence * The current trigger sequence; may be <code>null</code> or * empty. */ private final void updateTableBindingsForTriggerSequence( final TriggerSequence triggerSequence) { // Clear the table of its existing items. tableBindingsForTriggerSequence.removeAll(); // Get the collection of bindings for the current command. final Map activeBindings = localChangeManager .getActiveBindingsDisregardingContext(); final Collection bindings = (Collection) activeBindings .get(triggerSequence); if (bindings == null) { return; } // Add each of the bindings. final Iterator bindingItr = bindings.iterator(); while (bindingItr.hasNext()) { final Binding binding = (Binding) bindingItr.next(); final Context context = contextService.getContext(binding .getContextId()); final ParameterizedCommand parameterizedCommand = binding .getParameterizedCommand(); final Command command = parameterizedCommand.getCommand(); if ((!context.isDefined()) && (!command.isDefined())) { continue; } final TableItem tableItem = new TableItem( tableBindingsForTriggerSequence, SWT.NULL); tableItem.setData(ITEM_DATA_KEY, binding); /* * Set the associated image based on the type of binding. Either it * is a user binding or a system binding. * * TODO Identify more image types. */ if (binding.getType() == Binding.SYSTEM) { tableItem.setImage(0, IMAGE_BLANK); } else { tableItem.setImage(0, IMAGE_CHANGE); } try { tableItem.setText(1, context.getName()); tableItem.setText(2, parameterizedCommand.getName()); } catch (final NotDefinedException e) { throw new Error( "Context or command became undefined on a non-UI thread while the UI thread was processing."); //$NON-NLS-1$ } } } /** * Updates the contents of the view tab. This queries the command manager * for a list of key sequence binding definitions, and these definitions are * then added to the table. * * @since 3.1 */ private final void updateViewTab() { // Clear out the existing table contents. tableBindings.removeAll(); // Get a sorted list of key binding contents. final List bindings = new ArrayList(localChangeManager .getActiveBindingsDisregardingContextFlat()); Collections.sort(bindings, new Comparator() { /** * Compares two instances of <code>Binding</code> based on the * current sort order. * * @param object1 * The first object to compare; must be an instance of * <code>Binding</code> (i.e., not <code>null</code>). * @param object2 * The second object to compare; must be an instance of * <code>Binding</code> (i.e., not <code>null</code>). * @return The integer value representing the comparison. The * comparison is based on the current sort order. * @since 3.1 */ @Override public final int compare(final Object object1, final Object object2) { final Binding binding1 = (Binding) object1; final Binding binding2 = (Binding) object2; /* * Get the category name, command name, formatted key sequence * and context name for the first binding. */ final Command command1 = binding1.getParameterizedCommand() .getCommand(); String categoryName1 = Util.ZERO_LENGTH_STRING; String commandName1 = Util.ZERO_LENGTH_STRING; try { commandName1 = command1.getName(); categoryName1 = command1.getCategory().getName(); } catch (final NotDefinedException e) { // Just use the zero-length string. } final String triggerSequence1 = binding1.getTriggerSequence() .format(); final String contextId1 = binding1.getContextId(); String contextName1 = Util.ZERO_LENGTH_STRING; if (contextId1 != null) { final Context context = contextService .getContext(contextId1); try { contextName1 = context.getName(); } catch (final org.eclipse.core.commands.common.NotDefinedException e) { // Just use the zero-length string. } } /* * Get the category name, command name, formatted key sequence * and context name for the first binding. */ final Command command2 = binding2.getParameterizedCommand() .getCommand(); String categoryName2 = Util.ZERO_LENGTH_STRING; String commandName2 = Util.ZERO_LENGTH_STRING; try { commandName2 = command2.getName(); categoryName2 = command2.getCategory().getName(); } catch (final org.eclipse.core.commands.common.NotDefinedException e) { // Just use the zero-length string. } final String keySequence2 = binding2.getTriggerSequence() .format(); final String contextId2 = binding2.getContextId(); String contextName2 = Util.ZERO_LENGTH_STRING; if (contextId2 != null) { final Context context = contextService .getContext(contextId2); try { contextName2 = context.getName(); } catch (final org.eclipse.core.commands.common.NotDefinedException e) { // Just use the zero-length string. } } // Compare the items in the current sort order. int compare = 0; for (int element : sortOrder) { switch (element) { case VIEW_CATEGORY_COLUMN_INDEX: compare = Util.compare(categoryName1, categoryName2); if (compare != 0) { return compare; } break; case VIEW_COMMAND_COLUMN_INDEX: compare = Util.compare(commandName1, commandName2); if (compare != 0) { return compare; } break; case VIEW_KEY_SEQUENCE_COLUMN_INDEX: compare = Util.compare(triggerSequence1, keySequence2); if (compare != 0) { return compare; } break; case VIEW_CONTEXT_COLUMN_INDEX: compare = Util.compare(contextName1, contextName2); if (compare != 0) { return compare; } break; default: throw new Error( "Programmer error: added another sort column without modifying the comparator."); //$NON-NLS-1$ } } return compare; } /** * @see Object#equals(java.lang.Object) */ @Override public final boolean equals(final Object object) { return super.equals(object); } }); // Add a table item for each item in the list. final Iterator keyBindingItr = bindings.iterator(); while (keyBindingItr.hasNext()) { final Binding binding = (Binding) keyBindingItr.next(); // Get the command and category name. final ParameterizedCommand command = binding .getParameterizedCommand(); String commandName = Util.ZERO_LENGTH_STRING; String categoryName = Util.ZERO_LENGTH_STRING; try { commandName = command.getName(); categoryName = command.getCommand().getCategory().getName(); } catch (final org.eclipse.core.commands.common.NotDefinedException e) { // Just use the zero-length string. } // Ignore items with a meaningless command name. if ((commandName == null) || (commandName.length() == 0)) { continue; } // Get the context name. final String contextId = binding.getContextId(); String contextName = Util.ZERO_LENGTH_STRING; if (contextId != null) { final Context context = contextService.getContext(contextId); try { contextName = context.getName(); } catch (final org.eclipse.core.commands.common.NotDefinedException e) { // Just use the zero-length string. } } // Create the table item. final TableItem item = new TableItem(tableBindings, SWT.NONE); item.setText(VIEW_CATEGORY_COLUMN_INDEX, categoryName); item.setText(VIEW_COMMAND_COLUMN_INDEX, commandName); item.setText(VIEW_KEY_SEQUENCE_COLUMN_INDEX, binding .getTriggerSequence().format()); item.setText(VIEW_CONTEXT_COLUMN_INDEX, contextName); item.setData(BINDING_KEY, binding); } // Pack the columns. for (int i = 0; i < tableBindings.getColumnCount(); i++) { tableBindings.getColumn(i).pack(); } } }